今天刚学到作用域插槽的概念,结果跑到官网文档一看:

在 2.6.0 中,我们为具名插槽和作用域插槽引入了一个新的统一的语法 (即 v-slot 指令)。它取代了 slot 和 slot-scope 这两个目前已被废弃但未被移除且仍在文档中的特性。

看到这里,突然有了一点想法。
为什么知识越学越多,因为学习的速度很难赶上技术迭代的速度,而且你越晚学习越会有这种感觉。Vue 3.0 的源码在前几天公布(国庆期间)了,根据群里大佬的说法,明年第一季度很可能就正式发布 3.0,似乎暗示着又一波学习热潮即将到来。工作以后和业务打交道,其实很少有时间可以学习了,所以现在 —— 大学的时间真的很宝贵,得好好珍惜。

为什么需要插槽?

我们先来看下这幅图:

可以看到,每个页面都有对应的 nav-bar,所以这应该是一个可以复用的组件。问题是怎么复用呢?统一封装成一个组件肯定不行,因为这些 nav-bar 结构和内容并不完全相同;针对每个页面都封装一个组件也不行,因为它们有相同的结构和内容,这不利于复用。

所以我们需要的其实是一个足够灵活的组件,其内容可以更进一步地自定义,就像是积木一样,对于同一个位置 C,我们可以安插 A ,也可以安插 B。而插槽(slot) 就是用来实现这种灵活性的。

插槽是子组件暴露的一个让父组件传入自定义内容的接口。我们可以形象地把它理解成电脑的各种接口,提供了各种扩展。

单个插槽

// 子组件
<template>
  <slot></slot>
</template>

// 父组件
<template>
  <cpn>
    <span>我是父组件传给子组件插槽的内容<span>
  </cpn>
</template>

<slot></slot> 相当于一个空缺,等待父组件给它填充内容。当然也可以给 <slot></slot> 指定默认值,例如 <slot>我是默认值</slot>,这种情况下,其将在父组件未传入内容时(即 <cpn></cpn>)得到应用。

子组件可有多个 slot,这些 slot 都将使用父组件传入的内容。

具名插槽

大部分时候,我们需要给特定的插槽传入特定的内容,所以每个插槽必须得有一个名字作为标识,这时候就要使用具名插槽了。具体来说,就是给 slot 添加 name 属性,之后在父组件中真正传入内容的时候,将内容包裹在有 slot="name" 属性的元素中。
假设我们要用同一个组件实现下面三种不同效果:

那么,首先会想到给这个子组件三个插槽,由于特定的插槽要传入特定的内容,所以我们这里使用具名插槽。代码如下:

<!--父组件模板-->
<div id="app">
  <cpn>
    <span slot="right">5</span>
  </cpn>
  <cpn>
    <span slot="left">4</span>
    <span slot="right">6</span>
  </cpn>
  <cpn>
    <span slot="center">3</span>
    <span slot="right">7</span>
  </cpn>
</div>

<!--子组件模板-->
<template id="cpn">
  <div>
    <h2>我是子组件</h2>
    <slot name="left">1</slot>
    <slot name="center">2</slot>
    <slot name="right">3</slot>
  </div>
</template>

效果为:

注意

  • Vue 2.6.0 之后改 slotv-slot,且必须绑定在一个 template 元素上:

    <span slot="right">5</span> 
    
    <!--改为-->
    
    <template v-slot:right>
        <span>5</span>
    </template>
  • 另外,我们可以将 v-slot:right 直接缩写为 #right,注意这仅适用于 v-slot 有参数的情况,例如 v-slot="xxx" 是不能缩写的。

编译作用域

关于编译作用域,只需要记住一条规则:

父级模板里的所有内容都是在父级作用域中编译的;子模板里的所有内容都是在子作用域中编译的。

来看一个例子:

<!--HTML-->
<div id="app">
  <cpn v-if="isShow"></cpn>
</div>	
// JS
const app = new Vue({
  el:'#app',
  components:{
    cpn:{
      template:"#cpn",
      data(){
        return {
          isShow:true
        }
      }
    }
  },
  data:{
    isShow:false
  }
})

对于这个例子,组件最后到底会不会显示(渲染)呢?
答案是不会。尽管父组件和子组件都有 isShow 这个变量,且后者为 true,但子组件是存在于父级模板中的,其内容是在父级作用域中编译的,只能识别父级作用域中为 false 的那个 isShow

在这里,我们知道父模板无法直接访问子组件中的数据,但是有了作用域插槽之后,又不一样了。

作用域插槽

我们先来设想一种情况。假定子组件中有数据:

languages:["Java","C","Python","Swift"]

然后,现在要求在父组件中以不同的形式将这些数据进行展示,可能是列表,也可能是互相之间以斜杆分隔。如下图:

首先,数据的呈现方式不同,也即 HTML 结构不同,因此不能直接在子组件模板中书写结构,这时候想到了应该给子组件一个插槽,后面在父组件模板中再定义结构。但这样一来,父组件无可避免地要使用子组件的数据,而前面说过编译作用域的问题,所以这里的父组件实际上是无法拿到子组件数据的。怎么办呢?

这时候,作用域插槽就派上用场了。作用域插槽可以理解为是带数据的插槽,它允许我们在父组件模板中访问子组件的数据。典型的应用场景就是,数据在子组件中,但是渲染数据的工作必须由父组件完成。

实现分为两步:

  • 将子组件数据绑定给 slot 上的属性(成为 插槽 prop
  • 父组件模板中通过 slot-scope 拿到 slot 对象(准确地说是包含所有 插槽prop 的对象)并进行属性访问

以这道题为例:

// JS
const app = new Vue({
  el:'#app',
  components:{
    cpn:{
      template:"#cpn",
      data(){
        return {
          languages:["Java","C","Python","Swift"]
        }
      }
    }
  }
})

首先,我们通过 :lang="languages" 将子组件的 languages 绑定到 slotlang 属性中。

<!--子组件模板-->
<template id="cpn">
  <div>
    <h2>展示方式:</h2>
    <slot :lang="languages"></slot>
  </div>
</template>

接着,父组件模板中给插槽传入 template,这个 template 带有 slot-scope="obj",使得我们可以通过 obj.lang 访问到子组件数据。

<div id="app">
  <!--展示方式一-->
  <cpn>
    <template slot-scope="obj">
      <ul>
        <li v-for="item in obj.lang">{{item}}</li>
      </ul>
    </template>
  </cpn>
  <!--展示方式二-->
  <cpn>
    <template slot-scope="obj">
        {{obj.lang.join('/')}}
    </template>
  </cpn>
  <!--展示方式三-->
  <cpn>
    <template slot-scope="obj">
        {{obj.lang.join('----')}}
    </template>
  </cpn>
</div>	

最终实现我们想要的效果。
当然,关于作用域插槽还有一些地方需要注意:

  • 如果数据过多,不可能一一绑定,这时候直接 v-bind 到一个属性就行,这个操作等同于手动绑定所有数据,方便了我们的访问。上例如果是 <slot v-bind:obj></slot>,后面直接 obj.languages 就能拿到数据了

  • Vue 2.6.0 之后,改 slot-scopev-slot,对于像上面一样的匿名插槽,只需要使用 v-slot="obj";对于具名插槽,则使用 v-slot:name="obj"。当然,v-slot:name 依然是表示具名插槽。

  • 正如前面所说,v-slot:name="obj" 也可以缩写为 #name="obj"

  • 另外,还有解构插槽、动态插槽名等,具体可以看文档

关于作用域插槽的应用,还有一个不错的案例,具体可以看下我之前翻译的一篇文章。我觉得里面有句话说得很有道理:

A good approach when you can’t understand something easily is to try put it to use in solving a problem.

大意是:当知识难以理解的时候,最好的办法就是拿它去解决问题。

我在这篇文章中其实也是尽量按照这个思路布局的,首先是刻意制造了一个问题,按照常规的思路没办法解决,接着引出相关概念,在实际情境中体会它的应用,这时候会有一种“噢,原来xxx可以解决这类型问题”的感觉,我觉得这或许是一种不错的学习方式,毕竟很多东西是为了解决问题而存在的,若能从最初问题产生的源头开始思考,兴许我们可以更好地理解它。